/********************************************************************************************************************************
 * 《深入Linux设备驱动程序内核机制》-- 陈学松 著，电子工业出版社， 2012年1月第1次印刷
 * Date: 2024-04-18 18:42
 * Author: dimon.chen 
 * EMali: 1181302388@qq.com 
 * 
 * 章节: 第二章 内核内存管理
 *       
 * 
 ********************************************************************************************************************************/



//参考链接：
//  CNDN博客:https://blog.csdn.net/vichie2008/article/details/44461961
//  练习和测试Linux内核内存管理，物理地址分配 alloc_pages(), alloc_page(), kmap(), kunmap()
//


// 以 X86 32bit 系统为例，处理器的地址总线的是32位，可以访问的地址空间是 0 到 4GB， 
// X86 32bit Linux操作系统把这个4GB的地址空间 0x0000 0000 ~ 0xffff ffff，给分为内核地址空间
// 和用户地址空间。
// X86 32bit 系统中处理器启用MMU（Mmemory Manage Unit）之后处理器访问的都是虚拟地址，
// 虚拟地址通过 MMU 和 页目录表/页面 转换为物理地址。
// 4GB 的虚拟地址空间分为 1GB + 3GB 两部分，0到3GB地址虚拟地址空间给用户进程使用，3GB - 4GB(长度1GB)
// 的虚拟地址空间给内核使用。
// 每个用户进程都有独立的虚拟地址空间，所有的用户进程和内核共享1GB的内核虚拟地址空间。
//
// Linux为了统一管理物理内存，把NUMA和UMA物理内存管理，通过 node, zone, page 的方式组织起来。
// 在NUMA内存模型中，每一个处理器的节点有一个内存node。
// 在UMA内存模型中，所有处理器访问共同的内存，只有一个内存node节点。
// 由于处理器体系结构的一些限制，例如 Intel X86 32bit PC 系统中 ISA 总线设备只能物理内存低地址空间
// 0~16MB的内存。
// Linux把物理内存分为不同内存区域Zone，
// ZONE_DMA        0到16MB
// ZONE_NORMAL     16MB到896MB
// ZONE_HIGHMEM     896MB-1024MB 
// 
// ZONE_DMA和ZONE_NORMAL低端内存区域和内核虚拟地址空间做线性行社，在内核启动初始化阶段会把 0 ~ 896MB
// 的地段内存区域线性映射到内核虚地址空间的前896MB
// 在线性地址映射区，物理地址和内核虚拟地址之间有一个固定的偏移 0xC000 0000 ，
// 通过 virtual_addr = phy_addr + 0xc000 0000 
// 
// 在896MB ~ 1024MB之间的 128MB 可以动态映射到其它物理内存
// 当物理内存大于1GB时，内核虚拟地址空间却只有1GB，内核如何访问大于1GB的物理内存空间。
// 内核的虚拟地址空间大于 0xc000 0000 +896MB ，还有128MB的虚拟地址空间，用这128MB的虚拟地址空间
// 来动态映射到高端物理内存去。
// 就类似杂耍抛球，有两只手但是却有多只球抛来抛去，通过动态映射的方法，Linux内核使用高端的128MB虚拟地址
// 映射高端物理内存地址。
//
// PAGE_OFFSET = 896MB
// 在PAGE_OFFSET之后有一个8MB的空洞，防止访问越界
// 然后 KMALLOC_START, KMALLOC_END 动态映射区
// 然后是 4MB 的空洞
// 然后 PKMAP_START, PKMAP 持久映射区
// 然后是4MB空洞
// 然后是 FixMapping_START, FixMapping_END 静态映射区
// 

// KMAP 持久映射区的大小是4MB，有1024个页
// 当 alloc_page(__GFP_HIGHMEM) 申请到从 ZONE_HIGHMEM 高端内存去申请到一个物理页之后，
// 应为该物理页是没有线性映射的，所以需要调用kmap()先在内核地址空间申请一个虚拟地址页，然后通过 
// 把物理页和内核虚拟页映射起来，并更新内核页面。
// kmap() 调用可能会sleep，所以不能用在中断上下文中。
//


#include <linux/types.h>
#include <linux/delay.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <linux/list.h>
#include <linux/pci.h>
#include <linux/gpio.h>
#include <linux/gfp.h>
#include <asm/highmem.h>

struct page *my_map_high_mem(int order)
{
    int i =0;
    static int posion = 0x5a;
    unsigned char *buf = NULL;

    //通过GFP_KERNEL 和 __GFP_HIGHM指示从 ZONE_HIGMEM中分配高端物理页
    struct page *high_page = alloc_pages(GFP_KERNEL|__GFP_HIGHME, order);

    if(high_page){
        printk("high_page alloc success\n");
    }
    else{
        printk("high_page alloc fail\n");
        return NULL;
    }

    //kmap()为高端内存物理页，分配一个内核虚拟地址页，并建立映射关系
    //kmap()一次映射一个页，kmap调用可能会休眠
    buf = kmap(high_page);
    if(buf){
        //如果映射成功，kmap返回映射后的虚拟地址
        printk("kmap success, buf addr:%x\n", buf);

        for(i=0; i<4096; i++)
            buf[i] = posion;
        
        posion++;
    }
    else{
        printk("kmap fail\n");
        __free_pages(high_page); //如果kmap映射失败，返回NULL，并释放物理页
        return NULL;
    }

    return high_page;
}

void my_free_high_mem(struct page *page, int order)
{
    kunmap(page);
    __free_pages(page, order);
}


